<template>
  <div class="page">
    <save-request-as
      v-bind:show="showRequestModal"
      v-on:hide-model="hideRequestModal"
      v-bind:editing-request="editRequest"
    ></save-request-as>

    <pw-modal v-if="showModal" @close="showModal = false">
      <div slot="header">
        <ul>
          <li>
            <div class="flex-wrap">
              <h3 class="title">Import cURL</h3>
              <div>
                <button class="icon" @click="toggleModal">
                  <i class="material-icons">close</i>
                </button>
              </div>
            </div>
          </li>
        </ul>
      </div>
      <div slot="body">
        <ul>
          <li>
            <textarea id="import-text" autofocus rows="8" placeholder="Enter cURL"></textarea>
          </li>
        </ul>
      </div>
      <div slot="footer">
        <ul>
          <li>
            <button class="icon" @click="handleImport">
              <i class="material-icons">get_app</i>
              <span>Import</span>
            </button>
          </li>
        </ul>
      </div>
    </pw-modal>

    <pw-section v-if="showPreRequestScript" class="orange" label="Pre-Request" ref="preRequest">
      <ul>
        <li>
          <div class="flex-wrap">
            <label for="generatedCode">JavaScript Code</label>
            <div>
              <a
                href="https://github.com/liyasthomas/postwoman/wiki/Pre-Request-Scripts"
                target="_blank"
                rel="noopener"
              >
                <button class="icon" v-tooltip="'Wiki'">
                  <i class="material-icons">help</i>
                </button>
              </a>
            </div>
          </div>
          <textarea
            id="preRequestScript"
            @keydown="formatRawParams"
            rows="8"
            v-model="preRequestScript"
            v-textarea-auto-height="preRequestScript"
            spellcheck="false"
            placeholder="pw.environment.set('variable', 'value');"
          ></textarea>
        </li>
      </ul>
    </pw-section>

    <pw-section class="blue" label="Request" ref="request">
      <ul>
        <li>
          <label for="method">Method</label>
          <select id="method" v-model="method" @change="methodChange">
            <option>GET</option>
            <option>HEAD</option>
            <option>POST</option>
            <option>PUT</option>
            <option>DELETE</option>
            <option>OPTIONS</option>
            <option>PATCH</option>
          </select>
        </li>
        <li>
          <label for="url">URL</label>
          <input
            :class="{ error: !isValidURL }"
            @keyup.enter="isValidURL ? sendRequest() : null"
            id="url"
            name="url"
            type="url"
            v-model="url"
          />
        </li>
        <li>
          <label for="path">Path</label>
          <input
            @keyup.enter="isValidURL ? sendRequest() : null"
            id="path"
            name="path"
            v-model="path"
            @input="pathInputHandler"
          />
        </li>
        <li>
          <label for="label">Label</label>
          <input id="label" name="label" type="text" v-model="label" placeholder="(optional)" />
        </li>
        <li>
          <label class="hide-on-small-screen" for="send">&nbsp;</label>
          <button :disabled="!isValidURL" @click="sendRequest" id="send" ref="sendButton">
            Send
            <span id="hidden-message">Again</span>
            <span>
              <i class="material-icons">send</i>
            </span>
          </button>
        </li>
      </ul>
      <div
        class="blue"
        label="Request Body"
        v-if="method === 'POST' || method === 'PUT' || method === 'PATCH'"
      >
        <ul>
          <li>
            <label for="contentType">Content Type</label>
            <autocomplete
              :source="validContentTypes"
              :spellcheck="false"
              v-model="contentType"
            >Content Type</autocomplete>
          </li>
        </ul>
        <ul>
          <li>
            <span>
              <pw-toggle
                :on="rawInput"
                @change="rawInput = $event"
              >Raw Input {{ rawInput ? "Enabled" : "Disabled" }}</pw-toggle>
            </span>
          </li>
        </ul>
        <div v-if="!rawInput">
          <ul>
            <li>
              <label for="reqParamList">Parameter List</label>
              <textarea
                id="reqParamList"
                readonly
                v-textarea-auto-height="rawRequestBody"
                v-model="rawRequestBody"
                placeholder="(add at least one parameter)"
                rows="1"
              ></textarea>
            </li>
          </ul>
          <ul v-for="(param, index) in bodyParams" :key="index">
            <li>
              <input
                :placeholder="'key '+(index+1)"
                :name="'bparam'+index"
                :value="param.key"
                @change="$store.commit('setKeyBodyParams', { index, value: $event.target.value })"
                @keyup.prevent="setRouteQueryState"
                autofocus
              />
            </li>
            <li>
              <input
                :placeholder="'value '+(index+1)"
                :id="'bvalue'+index"
                :name="'bvalue'+index"
                :value="param.value"
                @change="$store.commit('setValueBodyParams', { index, value: $event.target.value })"
                @keyup.prevent="setRouteQueryState"
              />
            </li>
            <div>
              <li>
                <button class="icon" @click="removeRequestBodyParam(index)" id="delParam">
                  <i class="material-icons">delete</i>
                </button>
              </li>
            </div>
          </ul>
          <ul>
            <li>
              <button class="icon" @click="addRequestBodyParam" name="addrequest">
                <i class="material-icons">add</i>
                <span>Add New</span>
              </button>
            </li>
          </ul>
        </div>
        <div v-else>
          <ul>
            <li>
              <label for="rawBody">Raw Request Body</label>
              <textarea
                id="rawBody"
                @keydown="formatRawParams"
                rows="8"
                v-model="rawParams"
                v-textarea-auto-height="rawParams"
              ></textarea>
            </li>
          </ul>
        </div>
      </div>
      <div class="flex-wrap">
        <div style="text-align: center;">
          <button
            class="icon"
            id="show-modal"
            @click="showModal = true"
            v-tooltip.bottom="'Import cURL'"
          >
            <i class="material-icons">import_export</i>
          </button>
          <button
            class="icon"
            id="code"
            v-on:click="isHidden = !isHidden"
            :disabled="!isValidURL"
            v-tooltip.bottom="{ content: isHidden ? 'Show Code' : 'Hide Code'}"
          >
            <i class="material-icons" v-if="isHidden">flash_on</i>
            <i class="material-icons" v-if="!isHidden">flash_off</i>
          </button>
          <button
            :class="'icon' + (showPreRequestScript ? ' info-response' : '')"
            id="preRequestScriptButton"
            v-tooltip.bottom="{ content: !showPreRequestScript ? 'Show Pre-Request Script' : 'Hide Pre-Request Script'}"
            @click="showPreRequestScript = !showPreRequestScript"
          >
            <i
              class="material-icons"
              :class="showPreRequestScript"
              v-if="!showPreRequestScript"
            >code</i>
            <i
              class="material-icons"
              :class="showPreRequestScript"
              v-if="showPreRequestScript"
            >close</i>
          </button>
        </div>
        <div style="text-align: center;">
          <button
            class="icon"
            @click="copyRequest"
            id="copyRequest"
            ref="copyRequest"
            :disabled="!isValidURL"
            v-tooltip.bottom="'Copy Request URL'"
          >
            <i class="material-icons">file_copy</i>
          </button>

          <button
            class="icon"
            @click="saveRequest"
            id="saveRequest"
            ref="saveRequest"
            :disabled="!isValidURL"
            v-tooltip.bottom="'Save to Collections'"
          >
            <i class="material-icons">save</i>
          </button>
          <button class="icon" @click="clearContent('', $event)" v-tooltip.bottom="'Clear All'">
            <i class="material-icons">clear_all</i>
          </button>
        </div>
      </div>
    </pw-section>

    <pw-section class="yellow" label="Code" ref="requestCode" v-if="!isHidden">
      <ul>
        <li>
          <label for="requestType">Request Type</label>
          <select id="requestType" v-model="requestType">
            <option>JavaScript XHR</option>
            <option>Fetch</option>
            <option>cURL</option>
          </select>
        </li>
      </ul>
      <ul>
        <li>
          <div class="flex-wrap">
            <label for="generatedCode">Generated Code</label>
            <div>
              <button
                class="icon"
                @click="copyRequestCode"
                id="copyRequestCode"
                ref="copyRequestCode"
                v-tooltip="'Copy code'"
              >
                <i class="material-icons">file_copy</i>
              </button>
            </div>
          </div>
          <textarea
            id="generatedCode"
            ref="generatedCode"
            name="generatedCode"
            rows="8"
            v-model="requestCode"
          ></textarea>
        </li>
      </ul>
    </pw-section>

    <br />

    <section id="options">
      <input id="tab-one" type="radio" name="grp" checked="checked" />
      <label for="tab-one">Authentication</label>
      <div class="tab">
        <br />

        <pw-section class="cyan" label="Authentication" ref="authentication">
          <ul>
            <li>
              <div class="flex-wrap">
                <label for="auth">Authentication Type</label>
                <div>
                  <button
                    class="icon"
                    @click="clearContent('auth', $event)"
                    v-tooltip.bottom="'Clear'"
                  >
                    <i class="material-icons">clear_all</i>
                  </button>
                </div>
              </div>
              <select id="auth" v-model="auth">
                <option>None</option>
                <option>Basic</option>
                <option>Bearer Token</option>
              </select>
            </li>
          </ul>
          <ul v-if="auth === 'Basic'">
            <li>
              <input placeholder="User" name="http_basic_user" v-model="httpUser" />
            </li>
            <li>
              <input
                placeholder="Password"
                name="http_basic_passwd"
                :type="passwordFieldType"
                v-model="httpPassword"
              />
            </li>
            <div>
              <li>
                <button
                  class="icon"
                  id="switchVisibility"
                  ref="switchVisibility"
                  @click="switchVisibility"
                >
                  <i class="material-icons" v-if="passwordFieldType === 'text'">visibility</i>
                  <i class="material-icons" v-if="passwordFieldType !== 'text'">visibility_off</i>
                </button>
              </li>
            </div>
          </ul>
          <ul v-if="auth === 'Bearer Token'">
            <li>
              <input placeholder="Token" name="bearer_token" v-model="bearerToken" />
            </li>
          </ul>
          <div class="flex-wrap">
            <pw-toggle :on="!urlExcludes.auth" @change="setExclude('auth', !$event)">Include in URL</pw-toggle>
          </div>
        </pw-section>
      </div>
      <input id="tab-two" type="radio" name="grp" />
      <label for="tab-two">Headers</label>
      <div class="tab">
        <br />

        <pw-section class="orange" label="Headers" ref="headers">
          <ul>
            <li>
              <div class="flex-wrap">
                <label for="headerList">Header List</label>
                <div>
                  <button
                    class="icon"
                    @click="clearContent('headers', $event)"
                    v-tooltip.bottom="'Clear'"
                  >
                    <i class="material-icons">clear_all</i>
                  </button>
                </div>
              </div>
              <textarea
                id="headerList"
                readonly
                v-textarea-auto-height="headerString"
                v-model="headerString"
                placeholder="(add at least one header)"
                rows="1"
              ></textarea>
            </li>
          </ul>
          <ul v-for="(header, index) in headers" :key="index">
            <li>
              <input
                :placeholder="'header '+(index+1)"
                :name="'header'+index"
                :value="header.key"
                @change="$store.commit('setKeyHeader', { index, value: $event.target.value })"
                @keyup.prevent="setRouteQueryState"
                autofocus
              />
            </li>
            <li>
              <input
                :placeholder="'value '+(index+1)"
                :name="'value'+index"
                :value="header.value"
                @change="$store.commit('setValueHeader', { index, value: $event.target.value })"
                @keyup.prevent="setRouteQueryState"
              />
            </li>
            <div>
              <li>
                <button class="icon" @click="removeRequestHeader(index)" id="header">
                  <i class="material-icons">delete</i>
                </button>
              </li>
            </div>
          </ul>
          <ul>
            <li>
              <button class="icon" @click="addRequestHeader">
                <i class="material-icons">add</i>
                <span>Add New</span>
              </button>
            </li>
          </ul>
        </pw-section>
      </div>
      <input id="tab-three" type="radio" name="grp" />
      <label for="tab-three">Parameters</label>
      <div class="tab">
        <br />

        <pw-section class="pink" label="Parameters" ref="parameters">
          <ul>
            <li>
              <div class="flex-wrap">
                <label for="paramList">Parameter List</label>
                <div>
                  <button
                    class="icon"
                    @click="clearContent('parameters', $event)"
                    v-tooltip.bottom="'Clear'"
                  >
                    <i class="material-icons">clear_all</i>
                  </button>
                </div>
              </div>
              <textarea
                id="paramList"
                readonly
                v-textarea-auto-height="queryString"
                v-model="queryString"
                placeholder="(add at least one parameter)"
                rows="1"
              ></textarea>
            </li>
          </ul>
          <ul v-for="(param, index) in params" :key="index">
            <li>
              <input
                :placeholder="'parameter '+(index+1)"
                :name="'param'+index"
                :value="param.key"
                @change="$store.commit('setKeyParams', { index, value: $event.target.value })"
                autofocus
              />
            </li>
            <li>
              <input
                :placeholder="'value '+(index+1)"
                :name="'value'+index"
                :value="param.value"
                @change="$store.commit('setValueParams', { index, value: $event.target.value })"
              />
            </li>
            <div>
              <li>
                <button class="icon" @click="removeRequestParam(index)" id="param">
                  <i class="material-icons">delete</i>
                </button>
              </li>
            </div>
          </ul>
          <ul>
            <li>
              <button class="icon" @click="addRequestParam">
                <i class="material-icons">add</i>
                <span>Add New</span>
              </button>
            </li>
          </ul>
        </pw-section>
      </div>
    </section>

    <br />

    <pw-section class="purple" id="response" label="Response" ref="response">
      <ul>
        <li>
          <label for="status">status</label>
          <input
            :class="statusCategory ? statusCategory.className : ''"
            :value="response.status || '(waiting to send request)'"
            ref="status"
            id="status"
            name="status"
            readonly
            type="text"
          />
        </li>
      </ul>
      <ul v-for="(value, key) in response.headers" :key="key">
        <li>
          <label :for="key">{{key}}</label>
          <input :id="key" :value="value" :name="key" readonly />
        </li>
      </ul>
      <ul v-if="response.body">
        <li>
          <div class="flex-wrap">
            <label for="body">response</label>
            <div>
              <button
                class="icon"
                @click="copyResponse"
                ref="copyResponse"
                v-if="response.body"
                v-tooltip="'Copy response'"
              >
                <i class="material-icons">file_copy</i>
              </button>
              <button
                class="icon"
                @click="downloadResponse"
                ref="downloadResponse"
                v-if="response.body"
                v-tooltip="'Download file'"
              >
                <i class="material-icons">get_app</i>
              </button>
            </div>
          </div>
          <div id="response-details-wrapper">
            <pre>
              <code
  ref="responseBody"
  id="body"
  rows="16"
  placeholder="(waiting to send request)"
>{{response.body}}</code>
            </pre>
            <iframe
              :class="{hidden: !previewEnabled}"
              class="covers-response"
              ref="previewFrame"
              src="about:blank"
            ></iframe>
          </div>
          <div class="align-right" v-if="response.body && responseType === 'text/html'">
            <button class="icon" @click.prevent="togglePreview">
              <i class="material-icons" v-if="!previewEnabled">visibility</i>
              <i class="material-icons" v-if="previewEnabled">visibility_off</i>
              <span>{{ previewEnabled ? 'Hide Preview' : 'Preview HTML' }}</span>
            </button>
          </div>
        </li>
      </ul>
    </pw-section>

    <br />

    <pw-section class="yellow" label="Collections" ref="collections">
      <collections />
    </pw-section>

    <br />

    <history @useHistory="handleUseHistory" ref="historyComponent"></history>
  </div>
</template>
<script>
import url from "url";
import querystring from "querystring";
import autocomplete from "../components/autocomplete";
import history from "../components/history";
import section from "../components/section";
import textareaAutoHeight from "../directives/textareaAutoHeight";
import toggle from "../components/toggle";
import modal from "../components/modal";
import collections from "../components/collections";
import saveRequestAs from "../components/collections/saveRequestAs";
import parseCurlCommand from "../assets/js/curlparser.js";
import hljs from "highlight.js";
import "highlight.js/styles/androidstudio.css";
import getEnvironmentVariablesFromScript from "../functions/preRequest";
import parseTemplateString from "../functions/templating";

const statusCategories = [
  {
    name: "informational",
    statusCodeRegex: new RegExp(/[1][0-9]+/),
    className: "info-response"
  },
  {
    name: "successful",
    statusCodeRegex: new RegExp(/[2][0-9]+/),
    className: "success-response"
  },
  {
    name: "redirection",
    statusCodeRegex: new RegExp(/[3][0-9]+/),
    className: "redir-response"
  },
  {
    name: "client error",
    statusCodeRegex: new RegExp(/[4][0-9]+/),
    className: "cl-error-response"
  },
  {
    name: "server error",
    statusCodeRegex: new RegExp(/[5][0-9]+/),
    className: "sv-error-response"
  },
  {
    // this object is a catch-all for when no other objects match and should always be last
    name: "unknown",
    statusCodeRegex: new RegExp(/.*/),
    className: "missing-data-response"
  }
];
const parseHeaders = xhr => {
  const headers = xhr
    .getAllResponseHeaders()
    .trim()
    .split(/[\r\n]+/);
  const headerMap = {};
  headers.forEach(line => {
    const parts = line.split(": ");
    const header = parts.shift().toLowerCase();
    const value = parts.join(": ");
    headerMap[header] = value;
  });
  return headerMap;
};
export const findStatusGroup = responseStatus =>
  statusCategories.find(status => status.statusCodeRegex.test(responseStatus));

export default {
  directives: {
    textareaAutoHeight
  },

  components: {
    "pw-section": section,
    "pw-toggle": toggle,
    "pw-modal": modal,
    history,
    autocomplete,
    collections,
    saveRequestAs
  },
  data() {
    return {
      showModal: false,
      showPreRequestScript: false,
      preRequestScript: "",
      copyButton: '<i class="material-icons">file_copy</i>',
      downloadButton: '<i class="material-icons">get_app</i>',
      doneButton: '<i class="material-icons">done</i>',
      isHidden: true,
      response: {
        status: "",
        headers: "",
        body: ""
      },
      previewEnabled: false,
      paramsWatchEnabled: true,

      /**
       * These are content types that can be automatically
       * serialized by postwoman.
       */
      knownContentTypes: [
        "application/json",
        "application/x-www-form-urlencoded"
      ],

      /**
       * These are a list of Content Types known to Postwoman.
       */
      validContentTypes: [
        "application/json",
        "application/hal+json",
        "application/xml",
        "application/x-www-form-urlencoded",
        "text/html",
        "text/plain"
      ],
      showRequestModal: false,
      editRequest: {},

      urlExcludes: {}
    };
  },
  watch: {
    urlExcludes: {
      deep: true,
      handler() {
        this.$store.commit("postwoman/applySetting", [
          "URL_EXCLUDES",
          Object.assign({}, this.urlExcludes)
        ]);
      }
    },
    contentType(val) {
      this.rawInput = !this.knownContentTypes.includes(val);
    },
    rawInput(status) {
      if (status && this.rawParams === "") this.rawParams = "{}";
      else this.setRouteQueryState();
    },
    "response.body": function(val) {
      var responseText =
        document.querySelector("div#response-details-wrapper pre code") != null
          ? document.querySelector("div#response-details-wrapper pre code")
          : null;
      if (responseText) {
        if (
          document.querySelector(".hljs") !== null &&
          responseText.innerHTML.indexOf('<span class="hljs') !== -1
        ) {
          responseText.removeAttribute("class");
          responseText.innerHTML = null;
          responseText.innerText = this.response.body;
        } else if (
          responseText &&
          this.response.body !== "(waiting to send request)" &&
          this.response.body !== "Loading..." &&
          this.response.body !== "See JavaScript console (F12) for details."
        ) {
          responseText.innerText =
            this.responseType === "application/json" ||
            this.responseType === "application/hal+json"
              ? JSON.stringify(this.response.body, null, 2)
              : this.response.body;
          hljs.highlightBlock(
            document.querySelector("div#response-details-wrapper pre code")
          );
        } else {
          responseText.innerText = this.response.body;
        }
      }
    },
    params: {
      handler: function(newValue) {
        if (!this.paramsWatchEnabled) {
          this.paramsWatchEnabled = true;
          return;
        }
        let path = this.path;
        let queryString = newValue
          .filter(({ key }) => !!key)
          .map(({ key, value }) => `${key}=${value}`)
          .join("&");
        queryString = queryString === "" ? "" : `?${queryString}`;
        if (path.includes("?")) {
          path = path.slice(0, path.indexOf("?")) + queryString;
        } else {
          path = path + queryString;
        }

        this.path = path;
      },
      deep: true
    },
    selectedRequest(newValue, oldValue) {
      // @TODO: Convert all variables to single request variable
      if (!newValue) return;
      this.url = newValue.url;
      this.path = newValue.path;
      this.method = newValue.method;
      this.auth = newValue.auth;
      this.httpUser = newValue.httpUser;
      this.httpPassword = newValue.httpPassword;
      this.passwordFieldType = newValue.passwordFieldType;
      this.bearerToken = newValue.bearerToken;
      this.headers = newValue.headers;
      this.params = newValue.params;
      this.bodyParams = newValue.bodyParams;
      this.rawParams = newValue.rawParams;
      this.rawInput = newValue.rawInput;
      this.contentType = newValue.contentType;
      this.requestType = newValue.requestType;
    },
    editingRequest(newValue) {
      this.editRequest = newValue;
      this.showRequestModal = true;
    }
  },
  computed: {
    url: {
      get() {
        return this.$store.state.request.url;
      },
      set(value) {
        this.$store.commit("setState", { value, attribute: "url" });
      }
    },
    method: {
      get() {
        return this.$store.state.request.method;
      },
      set(value) {
        this.$store.commit("setState", { value, attribute: "method" });
      }
    },
    path: {
      get() {
        return this.$store.state.request.path;
      },
      set(value) {
        this.$store.commit("setState", { value, attribute: "path" });
      }
    },
    label: {
      get() {
        return this.$store.state.request.label;
      },
      set(value) {
        this.$store.commit("setState", { value, attribute: "label" });
      }
    },
    auth: {
      get() {
        return this.$store.state.request.auth;
      },
      set(value) {
        this.$store.commit("setState", { value, attribute: "auth" });
      }
    },
    httpUser: {
      get() {
        return this.$store.state.request.httpUser;
      },
      set(value) {
        this.$store.commit("setState", { value, attribute: "httpUser" });
      }
    },
    httpPassword: {
      get() {
        return this.$store.state.request.httpPassword;
      },
      set(value) {
        this.$store.commit("setState", { value, attribute: "httpPassword" });
      }
    },
    bearerToken: {
      get() {
        return this.$store.state.request.bearerToken;
      },
      set(value) {
        this.$store.commit("setState", { value, attribute: "bearerToken" });
      }
    },
    headers: {
      get() {
        return this.$store.state.request.headers;
      },
      set(value) {
        this.$store.commit("setState", { value, attribute: "headers" });
      }
    },
    params: {
      get() {
        return this.$store.state.request.params;
      },
      set(value) {
        this.$store.commit("setState", { value, attribute: "params" });
      }
    },
    bodyParams: {
      get() {
        return this.$store.state.request.bodyParams;
      },
      set(value) {
        this.$store.commit("setState", { value, attribute: "bodyParams" });
      }
    },
    rawParams: {
      get() {
        return this.$store.state.request.rawParams;
      },
      set(value) {
        this.$store.commit("setState", { value, attribute: "rawParams" });
      }
    },
    rawInput: {
      get() {
        return this.$store.state.request.rawInput;
      },
      set(value) {
        this.$store.commit("setState", { value, attribute: "rawInput" });
      }
    },
    requestType: {
      get() {
        return this.$store.state.request.requestType;
      },
      set(value) {
        this.$store.commit("setState", { value, attribute: "requestType" });
      }
    },
    contentType: {
      get() {
        return this.$store.state.request.contentType;
      },
      set(value) {
        this.$store.commit("setState", { value, attribute: "contentType" });
      }
    },
    passwordFieldType: {
      get() {
        return this.$store.state.request.passwordFieldType;
      },
      set(value) {
        this.$store.commit("setState", {
          value,
          attribute: "passwordFieldType"
        });
      }
    },

    selectedRequest() {
      return this.$store.state.postwoman.selectedRequest;
    },
    editingRequest() {
      return this.$store.state.postwoman.editingRequest;
    },
    requestName() {
      return this.label;
    },
    statusCategory() {
      return findStatusGroup(this.response.status);
    },
    isValidURL() {
      if (this.showPreRequestScript) {
        // we cannot determine if a URL is valid because the full string is not known ahead of time
        return true;
      }
      const protocol = "^(https?:\\/\\/)?";
      const validIP = new RegExp(
        protocol +
          "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
      );
      const validHostname = new RegExp(
        protocol +
          "(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]).)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$"
      );
      return validIP.test(this.url) || validHostname.test(this.url);
    },
    hasRequestBody() {
      return ["POST", "PUT", "PATCH"].includes(this.method);
    },
    pathName() {
      return this.path.match(/^([^?]*)\??/)[1];
    },
    rawRequestBody() {
      const { bodyParams } = this;
      if (this.contentType === "application/json") {
        try {
          const obj = JSON.parse(
            `{${bodyParams
              .filter(({ key }) => !!key)
              .map(
                ({ key, value }) => `
              "${key}": "${value}"
              `
              )
              .join()}}`
          );
          return JSON.stringify(obj);
        } catch (ex) {
          return "invalid";
        }
      } else {
        return bodyParams
          .filter(({ key }) => !!key)
          .map(({ key, value }) => `${key}=${encodeURIComponent(value)}`)
          .join("&");
      }
    },
    headerString() {
      const result = this.headers
        .filter(({ key }) => !!key)
        .map(({ key, value }) => `${key}: ${value}`)
        .join(",\n");
      return result === "" ? "" : `${result}`;
    },
    queryString() {
      const result = this.params
        .filter(({ key }) => !!key)
        .map(({ key, value }) => `${key}=${encodeURIComponent(value)}`)
        .join("&");
      return result === "" ? "" : `?${result}`;
    },
    responseType() {
      return (this.response.headers["content-type"] || "")
        .split(";")[0]
        .toLowerCase();
    },
    requestCode() {
      if (this.requestType === "JavaScript XHR") {
        var requestString = [];
        requestString.push("const xhr = new XMLHttpRequest()");
        const user = this.auth === "Basic" ? "'" + this.httpUser + "'" : null;
        const pswd =
          this.auth === "Basic" ? "'" + this.httpPassword + "'" : null;
        requestString.push(
          "xhr.open('" +
            this.method +
            "', '" +
            this.url +
            this.pathName +
            this.queryString +
            "', true, " +
            user +
            ", " +
            pswd +
            ")"
        );
        if (this.auth === "Bearer Token") {
          requestString.push(
            "xhr.setRequestHeader('Authorization', 'Bearer " +
              this.bearerToken +
              "')"
          );
        }
        if (this.headers) {
          this.headers.forEach(function(element) {
            requestString.push(
              "xhr.setRequestHeader('" +
                element.key +
                "', '" +
                element.value +
                "')"
            );
          });
        }
        if (["POST", "PUT", "PATCH"].includes(this.method)) {
          const requestBody = this.rawInput
            ? this.rawParams
            : this.rawRequestBody;
          requestString.push(
            "xhr.setRequestHeader('Content-Length', " + requestBody.length + ")"
          );
          requestString.push(
            "xhr.setRequestHeader('Content-Type', '" +
              this.contentType +
              "; charset=utf-8')"
          );
          requestString.push("xhr.send(" + requestBody + ")");
        } else {
          requestString.push("xhr.send()");
        }
        return requestString.join("\n");
      } else if (this.requestType == "Fetch") {
        var requestString = [];
        var headers = [];
        requestString.push(
          'fetch("' + this.url + this.pathName + this.queryString + '", {\n'
        );
        requestString.push('  method: "' + this.method + '",\n');
        if (this.auth === "Basic") {
          var basic = this.httpUser + ":" + this.httpPassword;
          headers.push(
            '    "Authorization": "Basic ' +
              window.btoa(unescape(encodeURIComponent(basic))) +
              '",\n'
          );
        } else if (this.auth === "Bearer Token") {
          headers.push(
            '    "Authorization": "Bearer ' + this.bearerToken + '",\n'
          );
        }
        if (["POST", "PUT", "PATCH"].includes(this.method)) {
          const requestBody = this.rawInput
            ? this.rawParams
            : this.rawRequestBody;
          requestString.push("  body: " + requestBody + ",\n");
          headers.push('    "Content-Length": ' + requestBody.length + ",\n");
          headers.push(
            '    "Content-Type": "' + this.contentType + '; charset=utf-8",\n'
          );
        }
        if (this.headers) {
          this.headers.forEach(function(element) {
            headers.push(
              '    "' + element.key + '": "' + element.value + '",\n'
            );
          });
        }
        headers = headers.join("").slice(0, -2);
        requestString.push("  headers: {\n" + headers + "\n  },\n");
        requestString.push('  credentials: "same-origin"\n');
        requestString.push("}).then(function(response) {\n");
        requestString.push("  response.status\n");
        requestString.push("  response.statusText\n");
        requestString.push("  response.headers\n");
        requestString.push("  response.url\n\n");
        requestString.push("  return response.text()\n");
        requestString.push("}).catch(function(error) {\n");
        requestString.push("  error.message\n");
        requestString.push("})");
        return requestString.join("");
      } else if (this.requestType === "cURL") {
        var requestString = [];
        requestString.push("curl -X " + this.method + " \\\n");
        requestString.push(
          "  '" + this.url + this.pathName + this.queryString + "' \\\n"
        );
        if (this.auth === "Basic") {
          var basic = this.httpUser + ":" + this.httpPassword;
          requestString.push(
            "  -H 'Authorization: Basic " +
              window.btoa(unescape(encodeURIComponent(basic))) +
              "' \\\n"
          );
        } else if (this.auth === "Bearer Token") {
          requestString.push(
            "  -H 'Authorization: Bearer " + this.bearerToken + "' \\\n"
          );
        }
        if (this.headers) {
          this.headers.forEach(function(element) {
            requestString.push(
              "  -H '" + element.key + ": " + element.value + "' \\\n"
            );
          });
        }
        if (["POST", "PUT", "PATCH"].includes(this.method)) {
          const requestBody = this.rawInput
            ? this.rawParams
            : this.rawRequestBody;
          requestString.push(
            "  -H 'Content-Length: " + requestBody.length + "' \\\n"
          );
          requestString.push(
            "  -H 'Content-Type: " + this.contentType + "; charset=utf-8' \\\n"
          );
          requestString.push("  -d '" + requestBody + "' \\\n");
        }
        return requestString.join("").slice(0, -3);
      }
    }
  },
  methods: {
    scrollInto(view) {
      this.$refs[view].$el.scrollIntoView({
        behavior: "smooth"
      });
    },
    handleUseHistory({ label, method, url, path }) {
      this.label = label;
      this.method = method;
      this.url = url;
      this.path = path;
      this.scrollInto("request");
    },
    getVariablesFromPreRequestScript() {
      if (!this.preRequestScript) {
        return {};
      }
      return getEnvironmentVariablesFromScript(this.preRequestScript);
    },
    async makeRequest(auth, headers, requestBody, preRequestScript) {
      const requestOptions = {
        method: this.method,
        url: this.url + this.pathName + this.queryString,
        auth,
        headers,
        data: requestBody ? requestBody.toString() : null
      };
      if (preRequestScript) {
        const environmentVariables = getEnvironmentVariablesFromScript(
          preRequestScript
        );
        requestOptions.url = parseTemplateString(
          requestOptions.url,
          environmentVariables
        );
        requestOptions.data = parseTemplateString(
          requestOptions.data,
          environmentVariables
        );
        for (let k in requestOptions.headers) {
          const kParsed = parseTemplateString(k, environmentVariables);
          const valParsed = parseTemplateString(
            requestOptions.headers[k],
            environmentVariables
          );
          delete requestOptions.headers[k];
          requestOptions.headers[kParsed] = valParsed;
        }
      }
      if (typeof requestOptions.data === "string") {
        requestOptions.data = parseTemplateString(requestOptions.data);
      }
      const config = this.$store.state.postwoman.settings.PROXY_ENABLED
        ? {
            method: "POST",
            url: `https://postwoman.apollotv.xyz/`,
            data: requestOptions
          }
        : requestOptions;

      const response = await this.$axios(config);
      return this.$store.state.postwoman.settings.PROXY_ENABLED
        ? response.data
        : response;
    },
    async sendRequest() {
      this.$toast.clear();
      this.scrollInto("response");

      if (!this.isValidURL) {
        this.$toast.error("URL is not formatted properly", {
          icon: "error"
        });
        return;
      }

      // Start showing the loading bar as soon as possible.
      // The nuxt axios module will hide it when the request is made.
      this.$nuxt.$loading.start();

      if (this.$refs.response.$el.classList.contains("hidden")) {
        this.$refs.response.$el.classList.toggle("hidden");
      }
      this.previewEnabled = false;
      this.response.status = "Fetching...";
      this.response.body = "Loading...";

      const auth =
        this.auth === "Basic"
          ? {
              username: this.httpUser,
              password: this.httpPassword
            }
          : null;

      let headers = {};
      let headersObject = {};

      Object.keys(headers).forEach(id => {
        headersObject[headers[id].key] = headers[id].value;
      });
      headers = headersObject;

      // If the request has a body, we want to ensure Content-Length and
      // Content-Type are sent.
      let requestBody;
      if (this.hasRequestBody) {
        requestBody = this.rawInput ? this.rawParams : this.rawRequestBody;

        Object.assign(headers, {
          //'Content-Length': requestBody.length,
          "Content-Type": `${this.contentType}; charset=utf-8`
        });
      }

      // If the request uses a token for auth, we want to make sure it's sent here.
      if (this.auth === "Bearer Token")
        headers["Authorization"] = `Bearer ${this.bearerToken}`;

      headers = Object.assign(
        // Clone the app headers object first, we don't want to
        // mutate it with the request headers added by default.
        Object.assign({}, this.headers),

        // We make our temporary headers object the source so
        // that you can override the added headers if you
        // specify them.
        headers
      );

      Object.keys(headers).forEach(id => {
        headersObject[headers[id].key] = headers[id].value;
      });
      headers = headersObject;

      try {
        const startTime = Date.now();

        const payload = await this.makeRequest(
          auth,
          headers,
          requestBody,
          this.showPreRequestScript && this.preRequestScript
        );

        const duration = Date.now() - startTime;
        this.$toast.info(`Finished in ${duration}ms`, {
          icon: "done"
        });

        (() => {
          const status = (this.response.status = payload.status);
          const headers = (this.response.headers = payload.headers);

          // We don't need to bother parsing JSON, axios already handles it for us!
          const body = (this.response.body = payload.data);

          const date = new Date().toLocaleDateString();
          const time = new Date().toLocaleTimeString();

          // Addition of an entry to the history component.
          const entry = {
            label: this.requestName,
            status,
            date,
            time,
            method: this.method,
            url: this.url,
            path: this.path,
            usesScripts: Boolean(this.preRequestScript)
          };
          this.$refs.historyComponent.addEntry(entry);
        })();
      } catch (error) {
        console.error(error);
        if (error.response) {
          this.response.headers = error.response.headers;
          this.response.status = error.response.status;
          this.response.body = error.response.data;

          // Addition of an entry to the history component.
          const entry = {
            label: this.requestName,
            status: this.response.status,
            date: new Date().toLocaleDateString(),
            time: new Date().toLocaleTimeString(),
            method: this.method,
            url: this.url,
            path: this.path,
            usesScripts: Boolean(this.preRequestScript)
          };
          this.$refs.historyComponent.addEntry(entry);
          return;
        } else {
          this.response.status = error.message;
          this.response.body = "See JavaScript console (F12) for details.";
          this.$toast.error(error + " (F12 for details)", {
            icon: "error"
          });
          if (!this.$store.state.postwoman.settings.PROXY_ENABLED) {
            this.$toast.info("Try enabling Proxy", {
              icon: "help",
              duration: 5000,
              action: {
                text: "Settings",
                onClick: (e, toastObject) => {
                  this.$router.push({ path: "/settings" });
                }
              }
            });
          }
        }
      }
    },
    getQueryStringFromPath() {
      let queryString,
        pathParsed = url.parse(this.path);
      return (queryString = pathParsed.query ? pathParsed.query : "");
    },
    queryStringToArray(queryString) {
      let queryParsed = querystring.parse(queryString);
      return Object.keys(queryParsed).map(key => ({
        key: key,
        value: queryParsed[key]
      }));
    },
    pathInputHandler() {
      let queryString = this.getQueryStringFromPath(),
        params = this.queryStringToArray(queryString);

      this.paramsWatchEnabled = false;
      this.params = params;
    },
    addRequestHeader() {
      this.$store.commit("addHeaders", {
        key: "",
        value: ""
      });
      return false;
    },
    removeRequestHeader(index) {
      this.$store.commit("removeHeaders", index);
      this.$toast.error("Deleted", {
        icon: "delete"
      });
    },
    addRequestParam() {
      this.$store.commit("addParams", { key: "", value: "" });
      return false;
    },
    removeRequestParam(index) {
      this.$store.commit("removeParams", index);
      this.$toast.error("Deleted", {
        icon: "delete"
      });
    },
    addRequestBodyParam() {
      this.$store.commit("addBodyParams", { key: "", value: "" });
      return false;
    },
    removeRequestBodyParam(index) {
      this.$store.commit("removeBodyParams", index);
      this.$toast.error("Deleted", {
        icon: "delete"
      });
    },
    formatRawParams(event) {
      if (event.which !== 13 && event.which !== 9) {
        return;
      }
      const textBody = event.target.value;
      const textBeforeCursor = textBody.substring(
        0,
        event.target.selectionStart
      );
      const textAfterCursor = textBody.substring(event.target.selectionEnd);
      if (event.which === 13) {
        event.preventDefault();
        const oldSelectionStart = event.target.selectionStart;
        const lastLine = textBeforeCursor.split("\n").slice(-1)[0];
        const rightPadding = lastLine.match(/([\s\t]*).*/)[1] || "";
        event.target.value =
          textBeforeCursor + "\n" + rightPadding + textAfterCursor;
        setTimeout(
          () =>
            (event.target.selectionStart = event.target.selectionEnd =
              oldSelectionStart + rightPadding.length + 1),
          1
        );
      } else if (event.which === 9) {
        event.preventDefault();
        const oldSelectionStart = event.target.selectionStart;
        event.target.value = textBeforeCursor + "\xa0\xa0" + textAfterCursor;
        event.target.selectionStart = event.target.selectionEnd =
          oldSelectionStart + 2;
        return false;
      }
    },
    copyRequest() {
      if (navigator.share) {
        let time = new Date().toLocaleTimeString();
        let date = new Date().toLocaleDateString();
        navigator
          .share({
            title: `Postwoman`,
            text: `Postwoman • API request builder at ${time} on ${date}`,
            url: window.location.href
          })
          .then(() => {})
          .catch(console.error);
      } else {
        var dummy = document.createElement("input");
        document.body.appendChild(dummy);
        dummy.value = window.location.href;
        dummy.select();
        document.execCommand("copy");
        document.body.removeChild(dummy);
        this.$refs.copyRequest.innerHTML = this.doneButton;
        this.$toast.info("Copied to clipboard", {
          icon: "done"
        });
        setTimeout(
          () =>
            (this.$refs.copyRequest.innerHTML =
              '<i class="material-icons">file_copy</i>'),
          1000
        );
      }
    },
    copyRequestCode() {
      this.$refs.copyRequestCode.innerHTML = this.doneButton;
      this.$toast.success("Copied to clipboard", {
        icon: "done"
      });
      this.$refs.generatedCode.select();
      document.execCommand("copy");
      setTimeout(
        () => (this.$refs.copyRequestCode.innerHTML = this.copyButton),
        1000
      );
    },
    copyResponse() {
      this.$refs.copyResponse.innerHTML = this.doneButton;
      this.$toast.success("Copied to clipboard", {
        icon: "done"
      });
      var aux = document.createElement("textarea");
      var copy =
        this.responseType == "application/json"
          ? JSON.stringify(this.response.body)
          : this.response.body;
      aux.innerText = copy;
      document.body.appendChild(aux);
      aux.select();
      document.execCommand("copy");
      document.body.removeChild(aux);
      setTimeout(
        () => (this.$refs.copyResponse.innerHTML = this.copyButton),
        1000
      );
    },
    downloadResponse() {
      var dataToWrite = JSON.stringify(this.response.body, null, 2);
      var file = new Blob([dataToWrite], { type: this.responseType });
      var a = document.createElement("a"),
        url = URL.createObjectURL(file);
      a.href = url;
      a.download = (
        this.url +
        this.path +
        " [" +
        this.method +
        "] on " +
        Date()
      ).replace(".", "[dot]");
      document.body.appendChild(a);
      a.click();
      this.$refs.downloadResponse.innerHTML = this.doneButton;
      this.$toast.success("Download started", {
        icon: "done"
      });
      setTimeout(() => {
        document.body.removeChild(a);
        window.URL.revokeObjectURL(url);
        this.$refs.downloadResponse.innerHTML = this.downloadButton;
      }, 1000);
    },
    togglePreview() {
      this.previewEnabled = !this.previewEnabled;
      if (this.previewEnabled) {
        // If you want to add 'preview' support for other response types,
        // just add them here.
        if (this.responseType === "text/html") {
          // If the preview already has that URL loaded, let's not bother re-loading it all.
          if (
            this.$refs.previewFrame.getAttribute("data-previewing-url") ===
            this.url
          )
            return;
          // Use DOMParser to parse document HTML.
          const previewDocument = new DOMParser().parseFromString(
            this.response.body,
            this.responseType
          );
          // Inject <base href="..."> tag to head, to fix relative CSS/HTML paths.
          previewDocument.head.innerHTML =
            `<base href="${this.url}">` + previewDocument.head.innerHTML;
          // Finally, set the iframe source to the resulting HTML.
          this.$refs.previewFrame.srcdoc =
            previewDocument.documentElement.outerHTML;
          this.$refs.previewFrame.setAttribute("data-previewing-url", this.url);
        }
      }
    },
    setRouteQueryState() {
      const flat = key => (this[key] !== "" ? `${key}=${this[key]}&` : "");
      const deep = key => {
        const haveItems = [...this[key]].length;
        if (haveItems && this[key]["value"] !== "") {
          return `${key}=${JSON.stringify(this[key])}&`;
        } else return "";
      };
      let flats = [
        "method",
        "url",
        "path",
        !this.urlExcludes.auth ? "auth" : null,
        !this.urlExcludes.httpUser ? "httpUser" : null,
        !this.urlExcludes.httpPassword ? "httpPassword" : null,
        !this.urlExcludes.bearerToken ? "bearerToken" : null,
        "contentType"
      ]
        .filter(item => item !== null)
        .map(item => flat(item));
      let deeps = ["headers", "params"].map(item => deep(item));
      let bodyParams = this.rawInput
        ? [flat("rawParams")]
        : [deep("bodyParams")];

      this.$router.replace(
        "/?" +
          flats
            .concat(deeps, bodyParams)
            .join("")
            .slice(0, -1)
      );
    },
    setRouteQueries(queries) {
      if (typeof queries !== "object")
        throw new Error("Route query parameters must be a Object");
      for (const key in queries) {
        if (["headers", "params", "bodyParams"].includes(key))
          this[key] = JSON.parse(queries[key]);
        if (key === "rawParams") {
          this.rawInput = true;
          this.rawParams = queries["rawParams"];
        } else if (typeof this[key] === "string") this[key] = queries[key];
      }
    },
    observeRequestButton() {
      const requestElement = this.$refs.request.$el;
      const sendButtonElement = this.$refs.sendButton;
      const observer = new IntersectionObserver(
        (entries, observer) => {
          entries.forEach(entry => {
            if (entry.isIntersecting)
              sendButtonElement.classList.remove("show");
            // The button should float when it is no longer visible on screen.
            // This is done by adding the show class to the button.
            else sendButtonElement.classList.add("show");
          });
        },
        {
          rootMargin: "0px",
          threshold: [0]
        }
      );
      observer.observe(requestElement);
    },
    handleImport() {
      let textarea = document.getElementById("import-text");
      let text = textarea.value;
      try {
        let parsedCurl = parseCurlCommand(text);
        this.url = parsedCurl.url.replace(/"/g, "").replace(/'/g, "");
        this.url =
          this.url.slice(-1).pop() == "/" ? this.url.slice(0, -1) : this.url;
        this.path = "";
        this.headers = [];
        for (const key of Object.keys(parsedCurl.headers)) {
          this.$store.commit("addHeaders", {
            key: key,
            value: parsedCurl.headers[key]
          });
        }
        this.method = parsedCurl.method.toUpperCase();
        if (parsedCurl["data"]) {
          this.rawInput = true;
          this.rawParams = parsedCurl["data"];
        }
        this.showModal = false;
      } catch (error) {
        this.showModal = false;
        this.$toast.error("cURL is not formatted properly", {
          icon: "error"
        });
      }
    },
    toggleModal() {
      this.showModal = !this.showModal;
    },
    switchVisibility() {
      this.passwordFieldType =
        this.passwordFieldType === "password" ? "text" : "password";
    },
    clearContent(name, e) {
      switch (name) {
        case "auth":
          this.auth = "None";
          this.httpUser = "";
          this.httpPassword = "";
          this.bearerToken = "";
          break;
        case "headers":
          this.headers = [];
          break;
        case "parameters":
          this.params = [];
          break;
        default:
          (this.label = ""),
            (this.method = "GET"),
            (this.url = "https://reqres.in"),
            (this.auth = "None"),
            (this.path = "/api/users"),
            (this.auth = "None");
          this.httpUser = "";
          this.httpPassword = "";
          this.bearerToken = "";
          this.headers = [];
          this.params = [];
          this.bodyParams = [];
          this.rawParams = "";
      }
      e.target.innerHTML = this.doneButton;
      this.$toast.info("Cleared", {
        icon: "clear_all"
      });
      setTimeout(
        () => (e.target.innerHTML = '<i class="material-icons">clear_all</i>'),
        1000
      );
    },
    saveRequest() {
      this.editRequest = {
        url: this.url,
        path: this.path,
        method: this.method,
        auth: this.auth,
        httpUser: this.httpUser,
        httpPassword: this.httpPassword,
        passwordFieldType: this.passwordFieldType,
        bearerToken: this.bearerToken,
        headers: this.headers,
        params: this.params,
        bodyParams: this.bodyParams,
        rawParams: this.rawParams,
        rawInput: this.rawInput,
        contentType: this.contentType,
        requestType: this.requestType
      };

      if (this.selectedRequest.url) {
        this.editRequest = Object.assign(
          {},
          this.selectedRequest,
          this.editRequest
        );
      }

      this.showRequestModal = true;
    },
    hideRequestModal() {
      this.showRequestModal = false;
      this.editRequest = {};
    },
    setExclude(excludedField, excluded) {
      if (excludedField === "auth") {
        this.urlExcludes.auth = excluded;
        this.urlExcludes.httpUser = excluded;
        this.urlExcludes.httpPassword = excluded;
        this.urlExcludes.bearerToken = excluded;
      } else {
        this.urlExcludes[excludedField] = excluded;
      }
      this.setRouteQueryState();
    },
    methodChange() {
      // this.$store.commit('setState', { 'value': ["POST", "PUT", "PATCH"].includes(this.method) ? 'application/json' : '', 'attribute': 'contentType' })
      this.contentType = ["POST", "PUT", "PATCH"].includes(this.method)
        ? "application/json"
        : "";
    }
  },
  mounted() {
    this.observeRequestButton();
  },
  created() {
    this.urlExcludes = this.$store.state.postwoman.settings.URL_EXCLUDES || {
      // Exclude authentication by default for security reasons.
      auth: true,
      httpUser: true,
      httpPassword: true,
      bearerToken: true
    };

    if (Object.keys(this.$route.query).length)
      this.setRouteQueries(this.$route.query);
    this.$watch(
      vm => [
        vm.label,
        vm.method,
        vm.url,
        vm.auth,
        vm.path,
        vm.httpUser,
        vm.httpPassword,
        vm.bearerToken,
        vm.headers,
        vm.params,
        vm.bodyParams,
        vm.contentType,
        vm.rawParams
      ],
      val => {
        this.setRouteQueryState();
      }
    );
  }
};
</script>
